类与对象(上)

一、本节目标

  1. 面向过程和面向对象初步认识
  2. 类的引入
  3. 类的定义
  4. 类的访问限定符及封装
  5. 类的作用域
  6. 类的实例化
  7. 类的对象大小的计算
  8. 类成员函数的 this 指针

二、面向过程和面向对象初步认识(过程与面向对象编程)

面向过程编程(ProceduralProgramming)

  • 关注“过程”或“步骤”。
  • 将问题分解为函数,每个函数执行一个特定的任务。
  • 主要依赖函数调用,常见于 C 语言。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

// 面向过程编程: 通过函数处理数据
void process(int data)
{
printf("Processing data: %d\n", data);
}

int main() {
int data = 42;
process(data);
return 0;
}

面向对象编程(Object-OrientedProgramming)

  • 关注“对象”,将数据与操作数据的方法结合。
  • 通过对象之间的交互解决问题,常见于 C++。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

class Processor
{
public:
void process(int data)
{
cout << "Processing data: " << data << endl;
}
};

int main()
{
Processor p;
p.process(42);
return 0;
}
  • C 语言是 面向过程 的,关注 的是 过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
  • C++是 基于面向对象 的,关注 的是 对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

三、类的引入(Introduction to Classes)

C 语言结构体中只能定义变量,在 C++中,结构体内不仅可以定义变量,也可以定义函数。 例如: 之前在数据结构初阶中,用 C 语言方式实现的栈,结构体中只能定义变量;但在 C++中,structclass 都可以包含 变量函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <cstdlib> // for malloc and free
using namespace std;

typedef int DataType;

struct Stack
{
DataType* array;
size_t capacity;
size_t size;

// 初始化函数
void Init(size_t cap)
{
array = (DataType*)malloc(sizeof(DataType) * cap);
if (!array)
{
perror("malloc failed");
return;
}
capacity = cap;
size = 0;
}

// 压栈操作
void Push(const DataType& data)
{
array[size++] = data; // 简化的例子,未考虑扩容
}

// 取栈顶元素
DataType Top()
{
return array[size - 1];
}

// 销毁栈
void Destroy()
{
if (array)
{
free(array);
array = nullptr;
}
}
};

int main()
{
Stack s;
s.Init(10);
s.Push(1);
s.Push(2);
cout << "Top element: " << s.Top() << endl;
s.Destroy();
return 0;
}

上面结构体的定义,在 C++中更喜欢用 class 来代替

四、类的定义(Defining a Class)

类的定义class 是定义类的关键字。类是对象的蓝图,包含成员变量(属性)和成员函数(方法)。

  • 类定义语法
1
2
3
4
5
6
7
8
9
10
class ClassName
{
// 成员变量
// 成员函数
};
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,
注意类定义结束时后面分号不能省略。

类体中内容称为类的成员:类中的变量称为类的属性或成员变量;
类中的函数称为类的方法或者成员函数。

两种定义方式

  1. 在类体中定义成员函数:这样的函数可能被编译器视为 inline 内联函数。
  2. 类声明与定义分离:通常将类的声明放在 .h 文件中,成员函数的定义放在 .cpp 文件中。注意:成员函数名前需要加类名::

示例 1:类体中定义所有内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}

void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year, _month, _day;
};

int main()
{
Date today;
today.Init(2024, 9, 22);
today.Print();
return 0;
}

示例 2:类声明与定义分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Date.h
class Date
{
public:
void Init(int year, int month, int day);
void Print();
private:
int _year, _month, _day;
};
// Date.cpp
#include <iostream>
#include "Date.h"
using namespace std;

void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}

void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

int main() {
Date today;
today.Init(2024, 9, 22);
today.Print();
return 0;
}

五、类的访问限定符及封装(Access Specifiers and Encapsulation)

访问限定符 用于控制类的成员是否能够在类外部访问:

  • public:类外可以访问。
  • private:类外部不能直接访问,只能通过类的内部方法操作。
  • protected:类外部无法访问,但在继承中可以访问。

封装(Encapsulation):隐藏类的实现细节,仅对外提供公共接口,保证数据的安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}

void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year, _month, _day; // 这些变量无法在类外直接访问
};

int main()
{
Date today;
today.Init(2024, 9, 22);
today.Print(); // 只能通过Print()访问日期信息
return 0;
}

六、类的作用域(Class Scope)

类的成员定义在类的作用域内,在类外部使用成员函数时,必须用域操作 :: 指定该成员属于哪个作用类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person
{
public:
void SetName(const char* name);
void PrintName();

private:
char _name[20];
};

// 类外定义成员函数
void Person::SetName(const char* name)
{
strcpy(_name, name); // 设置名字
}

void Person::PrintName()
{
cout << "Name: " << _name << endl; // 打印名字
}

int main()
{
Person p;
p.SetName("Alice");
p.PrintName();
return 0;
}

七、类的实例化(Instantiation of Classes)

类的 实例化 是指 通过类的定义创建对象,分配实际的内存空间给成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Car
{
public:
void SetBrand(const char* brand)
{
strcpy(_brand, brand);
}

void Print()
{
cout << "Car brand: " << _brand << endl;
}

private:
char _brand[20];
};

int main()
{
Car car1, car2; // 实例化两个对象
car1.SetBrand("Toyota");
car2.SetBrand("Honda");

car1.Print();
car2.Print();
return 0;
}
  1. 类是对对象进行描述的,是一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没有分配实际的内存空间 来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。

​ 类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。

​ 谜语:”年纪不大,胡子一把,主人来了,就喊妈妈” 谜底:山羊

  1. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
1
2
3
4
5
6
int main()
{
Person._age = 100; // 编译失败:error C2059: 语法错误:“.”
return 0;
}
Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。
  1. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

八、类对象模型(Class Object Model)

类对象的大小 由成员变量的大小决定,成员函数的代码不会占用对象的存储空间。

  • 空类 的大小为 1 字节,确保每个对象都有唯一标识。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Empty {};

class A
{
private:
int _x;
char _y;
};

int main()
{
cout << "Size of Empty class: " << sizeof(Empty) << endl;
cout << "Size of A: " << sizeof(A) << endl; // 由于内存对齐的影响,可能比预期的更大
return 0;
}

结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。

结构体内存对齐规则(Struct Memory Alignment Rules)

内容对齐 的规则(【C 语言】结构体内存布局解析——字节对齐_字节对齐规则-CSDN 博客):

  1. 第一个成员从偏移量为 0 的位置开始。
  2. 其他成员遵循它们大小的整数倍对齐。
  3. 总大小为最大对齐数的整数倍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

struct S1 {
char c;
int i;
};

struct S2 {
int i;
char c;
};

int main() {
cout << "Size of S1: " << sizeof(S1) << endl; // 8字节,内存对齐使得char占4字节
cout << "Size of S2: " << sizeof(S2) << endl; // 8字节
return 0;
}

九、this 指针

在 C++中,this 指针是一个特殊的指针,用于指向调用成员函数的当前对象(当前对象)。它只在类的非静态成员函数下面可用,是传递方式的。是对 this 指针隐式的详细讲解,包括特性、与 C 语言的对比,以及常见的易错点。

1. 引出:什么是 this 指针?

当一个对象调用其类的非静态成员函数时,编译器会自动传递该对象的地址给函数。this 指针就是该对象的地址的成员函数。它可以用于在成员函数中调用该函数的对象的地址成员。

1
2
3
4
5
6
7
8
9
class MyClass
{
public:
int value;
void setValue(int value)
{
this->value = value; // 使用this指针,避免成员变量与参数重名冲突
}
};

2. 特性:

  • 隐式传递this 指针不需要显式声明,它在所有非静态成员函数中隐式可用。

  • 常量性this 指针是常量指针,无法修改其指向的对象。即 this 类型为 MyClass* const

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass
    {
    public:
    void func()
    {
    // this = nullptr; // 错误!无法修改this指针的指向
    }
    };
  • 指向当前对象this 指向调用该成员函数的当前对象。

    1
    2
    MyClass obj;
    obj.func(); // this指针指向obj
  • 在常量成员函数中的 this 指针:在常量成员函数中,this 指针是指向常量的指针,其类型为 const MyClass* const,表示不能 this 修改对象的数据成员。

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass 
    {
    public:
    void func() const
    {
    // this->value = 10; // 错误!无法修改常成员函数中的对象数据
    }
    };

3. 与 C 的对比:

C 语言本身不支持类和对象的概念,因此也不存在 this 指针。在 C 中,要模拟类似的行为,通常需要显式传递结构体指针来访问结构体的成员。

1
2
3
4
5
6
7
8
9
10
11
12
C++中的this指针对应于在C语言中手动传递对象指针给函数的做法:
// C语言模拟对象方法
struct MyClass
{
int value;
};

void setValue(struct MyClass* self, int value)
{
self->value = value;
}
在C++中,这种显着式的传递结构指针的方式通过this指针抓取方式和自动化,简化了编程。

4. 常见易错点:

  • 修改 this 指针this 是常量指针,不能修改其指向对象,错误的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass
    {
    public:
    void func()
    {
    // this = nullptr; // 错误!无法修改this指针的指向
    }
    };
  • 在静态成员函数中使用 this:静态成员函数属于类本身,而不是某个特定对象,因此,静态成员函数中没有 this 指针。如果尝试在静态成员函数中使用 this,会出现编译错误。

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass
    {
    public:
    static void staticFunc()
    {
    // this->value = 10; // 错误,静态成员函数没有this指针
    }
    };
  • 返回 *this:在链式调用时,经常会返回当前对象的引用,返回 *this 是合法的用法。常见的用法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MyClass
    {
    public:
    MyClass& setValue(int value)
    {
    this->value = value;
    return *this; // 返回当前对象的引用,支持链式调用
    }
    };
  • 在构造函数或解析构造函数中使用 this:在构造函数中使用 this 指针是安全的,但需要注意不要在构造函数中将 this 指针导出出去(比如在构造函数中调用虚函数)。在构造函数中,this 指向即将被关注的对象,因此要小心避免对已关注的资源操作。

5. 小结:

  • this 指针用于指向当前对象,并在非静态成员函数中隐式传递。
  • 它是一个常量指针,不能修改指向的对象。
  • 静态成员函数中没有 this 指针。
  • this 在 C++中简化了对象成员的访问,而在 C 语言中,需要手动传递结构体指针。

掌握 this 指针有助于理解对象成员的访问方式和 C++类的工作原理。

总结

本文介绍了对象编程中的类、对象、封装、作用域、实例化、对象模型、内存定位和 this 指针的详细内容和代码示例。你可以通过编写这些代码加深理解,并尝试修改运行和它们来更好地掌握这些概念。